/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ceylon.ide.eclipse.code.explorer;

import static org.eclipse.jdt.core.IJavaElement.PACKAGE_FRAGMENT_ROOT;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.packageview.ClassPathContainer;
import org.eclipse.jdt.internal.ui.packageview.LibraryContainer;
import org.eclipse.jdt.internal.ui.packageview.PackageFragmentRootContainer;
import org.eclipse.jdt.internal.ui.packageview.PackagesMessages;
import org.eclipse.jdt.internal.ui.workingsets.WorkingSetModel;
import org.eclipse.jdt.ui.PreferenceConstants;
import org.eclipse.jdt.ui.StandardJavaElementContentProvider;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.IBasicPropertyConstants;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.progress.UIJob;

/**
 * Content provider for the PackageExplorer.
 *
 * <p>
 * Since 2.1 this content provider can provide the children for flat or hierarchical
 * layout.
 * </p>
 *
 * @see org.eclipse.jdt.ui.StandardJavaElementContentProvider
 */
public class PackageExplorerContentProvider extends StandardJavaElementContentProvider implements IElementChangedListener, IPropertyChangeListener {

    protected static final int ORIGINAL= 0;
    protected static final int PARENT= 1 << 0;
    protected static final int GRANT_PARENT= 1 << 1;
    protected static final int PROJECT= 1 << 2;

    private TreeViewer fViewer;
    private Object fInput;
    private boolean fIsFlatLayout;
    private boolean fShowLibrariesNode;
    private boolean fFoldPackages;

    private Collection<Runnable> fPendingUpdates;

    private UIJob fUpdateJob;

    /**
     * Creates a new content provider for Java elements.
     * @param provideMembers if set, members of compilation units and class files are shown
     */
    public PackageExplorerContentProvider(boolean provideMembers) {
        super(provideMembers);
        fShowLibrariesNode= false;
        fIsFlatLayout= false;
        fFoldPackages= arePackagesFoldedInHierarchicalLayout();
        fPendingUpdates= null;
        JavaPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this);

        fUpdateJob= null;
    }

    private boolean arePackagesFoldedInHierarchicalLayout(){
        return PreferenceConstants.getPreferenceStore().getBoolean(PreferenceConstants.APPEARANCE_FOLD_PACKAGES_IN_PACKAGE_EXPLORER);
    }

    protected Object getViewerInput() {
        return fInput;
    }

    /* (non-Javadoc)
     * Function declared on IElementChangedListener.
     */
    public void elementChanged(final ElementChangedEvent event) {
        final ArrayList<Runnable> runnables= new ArrayList<Runnable>();
        try {
            // 58952 delete project does not update Package Explorer [package explorer]
            // if the input to the viewer is deleted then refresh to avoid the display of stale elements
            if (inputDeleted(runnables))
                return;

            processDelta(event.getDelta(), runnables);
        } catch (JavaModelException e) {
            JavaPlugin.log(e);
        } finally {
            executeRunnables(runnables);
        }
    }

    protected final void executeRunnables(final Collection<Runnable> runnables) {

        // now post all collected runnables
        Control ctrl= fViewer.getControl();
        if (ctrl != null && !ctrl.isDisposed()) {
            final boolean hasPendingUpdates;
            synchronized (this) {
                hasPendingUpdates= fPendingUpdates != null && !fPendingUpdates.isEmpty();
            }
            //Are we in the UIThread? If so spin it until we are done
            if (!hasPendingUpdates && ctrl.getDisplay().getThread() == Thread.currentThread() && !fViewer.isBusy()) {
                runUpdates(runnables);
            } else {
                synchronized (this) {
                    if (fPendingUpdates == null) {
                        fPendingUpdates= runnables;
                    } else {
                        fPendingUpdates.addAll(runnables);
                    }
                }
                postAsyncUpdate(ctrl.getDisplay());
            }
        }
    }
    private void postAsyncUpdate(final Display display) {
        if (fUpdateJob == null) {
            fUpdateJob= new UIJob(display, PackagesMessages.PackageExplorerContentProvider_update_job_description) {
                @Override
                public IStatus runInUIThread(IProgressMonitor monitor) {
                    TreeViewer viewer= fViewer;
                    if (viewer != null && viewer.isBusy()) {
                        schedule(100); // reschedule when viewer is busy: bug 184991
                    } else {
                        runPendingUpdates();
                    }
                    return Status.OK_STATUS;
                }
            };
            fUpdateJob.setSystem(true);
        }
        fUpdateJob.schedule();
    }

    /**
     * Run all of the runnables that are the widget updates. Must be called in the display thread.
     */
    public void runPendingUpdates() {
        Collection<Runnable> pendingUpdates;
        synchronized (this) {
            pendingUpdates= fPendingUpdates;
            fPendingUpdates= null;
        }
        if (pendingUpdates != null && fViewer != null) {
            Control control = fViewer.getControl();
            if (control != null && !control.isDisposed()) {
                runUpdates(pendingUpdates);
            }
        }
    }

    private void runUpdates(Collection<Runnable> runnables) {
        Iterator<Runnable> runnableIterator = runnables.iterator();
        while (runnableIterator.hasNext()){
            runnableIterator.next().run();
        }
    }


    private boolean inputDeleted(Collection<Runnable> runnables) {
        if (fInput == null)
            return false;
        if (fInput instanceof IJavaElement && ((IJavaElement) fInput).exists())
            return false;
        if (fInput instanceof IResource && ((IResource) fInput).exists())
            return false;
        if (fInput instanceof WorkingSetModel)
            return false;
        if (fInput instanceof IWorkingSet) // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=156239
            return false;
        postRefresh(fInput, ORIGINAL, fInput, runnables);
        return true;
    }

    /* (non-Javadoc)
     * Function declared on IContentProvider.
     */
    @Override
    public void dispose() {
        super.dispose();
        JavaCore.removeElementChangedListener(this);
        JavaPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this);
    }

    /* (non-Javadoc)
     * @see org.eclipse.jdt.ui.StandardJavaElementContentProvider#getPackageFragmentRootContent(org.eclipse.jdt.core.IPackageFragmentRoot)
     */
    @Override
    protected Object[] getPackageFragmentRootContent(IPackageFragmentRoot root) throws JavaModelException {
        ArrayList<Object> result= new ArrayList<Object>();
        if (fIsFlatLayout) {
            Object[] content = super.getPackageFragmentRootContent(root);
            List<IPackageFragment> defaultModulePackages = new ArrayList<IPackageFragment>();
            for (Object o: content) {
                if (o instanceof IPackageFragment) {
                    defaultModulePackages.add((IPackageFragment) o);
                }
            }
            for (Object o: content) {
                if (o instanceof IPackageFragment) {
                    IPackageFragment pf = (IPackageFragment) o;
                    if (pf.isDefaultPackage()) {
                        defaultModulePackages.remove(pf);
                        for (Object dpc: getPackageContent(pf)) {
                            result.add(dpc);
                        }
                    }
                    else {
                        for (Object f: pf.getNonJavaResources()) {
                            if (f instanceof IFile) {
                                if (((IFile) f).getName().equals("module.ceylon")) {
                                    result.add(pf);
                                    defaultModulePackages.remove(pf);
                                    for (IPackageFragment c: new ArrayList<IPackageFragment>(defaultModulePackages)) {
                                        if (pf.getPath().isPrefixOf(c.getPath())) {
                                            defaultModulePackages.remove(c);
                                        }
                                    }
                                    break;
                                }
                            }
                        }
                    }
                }
                else {
                    result.add(o);
                }
            }
            result.addAll(defaultModulePackages);
        }
        else {
            // hierarchical package mode
            getHierarchicalPackageChildren(root, null, result);
            if (!isProjectPackageFragmentRoot(root)) {
                Object[] nonJavaResources= root.getNonJavaResources();
                for (int i= 0; i < nonJavaResources.length; i++) {
                    result.add(nonJavaResources[i]);
                }
            }
        }
        return result.toArray();
    }

    /* (non-Javadoc)
     * @see org.eclipse.jdt.ui.StandardJavaElementContentProvider#getPackageContent(org.eclipse.jdt.core.IPackageFragment)
     */
    @Override
    protected Object[] getPackageContent(IPackageFragment fragment) throws JavaModelException {
        ArrayList<Object> result= new ArrayList<Object>();
        if (fIsFlatLayout) {
            result.addAll(Arrays.asList(super.getPackageContent(fragment)));
            for (Object f: fragment.getNonJavaResources()) {
                if (f instanceof IFile) {
                    if (((IFile) f).getName().equals("module.ceylon")) {
                        IPackageFragmentRoot root = (IPackageFragmentRoot)fragment.getAncestor(PACKAGE_FRAGMENT_ROOT);
                        for (Object o: super.getPackageFragmentRootContent(root)) {
                            if (o instanceof IPackageFragment) {
                                if (o!=fragment &&
                                        fragment.getPath().isPrefixOf(((IPackageFragment) o).getPath())) {
                                    result.add(o);
                                }
                            }
                        }
                        return result.toArray();
                    }
                }
            }
        }
        else {
            // hierarchical package mode
            getHierarchicalPackageChildren((IPackageFragmentRoot) fragment.getParent(), fragment, result);
            Object[] nonPackages= super.getPackageContent(fragment);
            if (result.isEmpty())
                return nonPackages;
            for (int i= 0; i < nonPackages.length; i++) {
                result.add(nonPackages[i]);
            }
        }
        return result.toArray();
    }

    /* (non-Javadoc)
     * @see org.eclipse.jdt.ui.StandardJavaElementContentProvider#getFolderContent(org.eclipse.core.resources.IFolder)
     */
    @Override
    protected Object[] getFolderContent(IFolder folder) throws CoreException {
        if (fIsFlatLayout) {
            return super.getFolderContent(folder);
        }

        // hierarchical package mode
        ArrayList<Object> result= new ArrayList<Object>();

        getHierarchicalPackagesInFolder(folder, result);
        Object[] others= super.getFolderContent(folder);
        if (result.isEmpty())
            return others;
        for (int i= 0; i < others.length; i++) {
            result.add(others[i]);
        }
        return result.toArray();
    }


    @Override
    public Object[] getChildren(Object parentElement) {
        try {
            if (parentElement instanceof IJavaModel)
                return concatenate(getJavaProjects((IJavaModel)parentElement), getNonJavaProjects((IJavaModel)parentElement));

            if (parentElement instanceof PackageFragmentRootContainer)
                return getContainerPackageFragmentRoots((PackageFragmentRootContainer)parentElement);

            if (parentElement instanceof IProject) {
                IProject project= (IProject) parentElement;
                if (project.isAccessible())
                    return project.members();
                return NO_CHILDREN;
            }

            return super.getChildren(parentElement);
        } catch (CoreException e) {
            return NO_CHILDREN;
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.jdt.ui.StandardJavaElementContentProvider#getPackageFragmentRoots(org.eclipse.jdt.core.IJavaProject)
     */
    @Override
    protected Object[] getPackageFragmentRoots(IJavaProject project) throws JavaModelException {
        if (!project.getProject().isOpen())
            return NO_CHILDREN;

        List<Object> result= new ArrayList<Object>();

        IPackageFragmentRoot[] roots= project.getPackageFragmentRoots();
        for (int i= 0; i < roots.length; i++) {
            IPackageFragmentRoot root= roots[i];
            IClasspathEntry classpathEntry= root.getRawClasspathEntry();
            int entryKind= classpathEntry.getEntryKind();
            if (entryKind == IClasspathEntry.CPE_CONTAINER) {
                // all ClassPathContainers are added later
            } else if (fShowLibrariesNode && (entryKind == IClasspathEntry.CPE_LIBRARY || entryKind == IClasspathEntry.CPE_VARIABLE)) {
                IResource resource= root.getResource();
                if (resource != null && project.getResource().equals(resource.getParent())) {
                    // show resource as child of project, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=141906
                    result.add(resource);
                } else {
                    // skip: will add the referenced library node later
                }
            } else {
                if (isProjectPackageFragmentRoot(root)) {
                    // filter out package fragments that correspond to projects and
                    // replace them with the package fragments directly
                    Object[] fragments= getPackageFragmentRootContent(root);
                    for (int j= 0; j < fragments.length; j++) {
                        result.add(fragments[j]);
                    }
                } else {
                    result.add(root);
                }
            }
        }

        if (fShowLibrariesNode) {
            result.add(new LibraryContainer(project));
        }

        // separate loop to make sure all containers are on the classpath (even empty ones)
        IClasspathEntry[] rawClasspath= project.getRawClasspath();
        for (int i= 0; i < rawClasspath.length; i++) {
            IClasspathEntry classpathEntry= rawClasspath[i];
            if (classpathEntry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
                result.add(new ClassPathContainer(project, classpathEntry));
            }
        }
        Object[] resources= project.getNonJavaResources();
        for (int i= 0; i < resources.length; i++) {
            result.add(resources[i]);
        }
        return result.toArray();
    }

    private Object[] getContainerPackageFragmentRoots(PackageFragmentRootContainer container) {
        return container.getChildren();
    }

    private Object[] getNonJavaProjects(IJavaModel model) throws JavaModelException {
        return model.getNonJavaResources();
    }

    @Override
    protected Object internalGetParent(Object element) {
        if (!fIsFlatLayout && element instanceof IPackageFragment) {
            return getHierarchicalPackageParent((IPackageFragment) element);
        } else if (element instanceof IPackageFragmentRoot) {
            // since we insert logical package containers we have to fix
            // up the parent for package fragment roots so that they refer
            // to the container and containers refer to the project
            IPackageFragmentRoot root= (IPackageFragmentRoot)element;

            try {
                IClasspathEntry entry= root.getRawClasspathEntry();
                int entryKind= entry.getEntryKind();
                if (entryKind == IClasspathEntry.CPE_CONTAINER) {
                    return new ClassPathContainer(root.getJavaProject(), entry);
                } else if (fShowLibrariesNode && (entryKind == IClasspathEntry.CPE_LIBRARY || entryKind == IClasspathEntry.CPE_VARIABLE)) {
                    return new LibraryContainer(root.getJavaProject());
                }
            } catch (JavaModelException e) {
                // fall through
            }
        } else if (element instanceof PackageFragmentRootContainer) {
            return ((PackageFragmentRootContainer)element).getJavaProject();
        }
        return super.internalGetParent(element);
    }

    /* (non-Javadoc)
     * Function declared on IContentProvider.
     */
    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        super.inputChanged(viewer, oldInput, newInput);
        fViewer= (TreeViewer)viewer;
        if (oldInput == null && newInput != null) {
            JavaCore.addElementChangedListener(this);
        } else if (oldInput != null && newInput == null) {
            JavaCore.removeElementChangedListener(this);
        }
        fInput= newInput;
    }

    // hierarchical packages
    /**
     * Returns the hierarchical packages inside a given fragment or root.
     *
     * @param parent the parent package fragment root
     * @param fragment the package to get the children for or 'null' to get the children of the root
     * @param result Collection where the resulting elements are added
     * @throws JavaModelException if fetching the children fails
     */
    private void getHierarchicalPackageChildren(IPackageFragmentRoot parent, IPackageFragment fragment, Collection<Object> result) throws JavaModelException {
        IJavaElement[] children= parent.getChildren();
        String prefix= fragment != null ? fragment.getElementName() + '.' : ""; //$NON-NLS-1$
        int prefixLen= prefix.length();
        for (int i= 0; i < children.length; i++) {
            IPackageFragment curr= (IPackageFragment) children[i];
            String name= curr.getElementName();
            if (name.startsWith(prefix) && name.length() > prefixLen && name.indexOf('.', prefixLen) == -1) {
                if (fFoldPackages) {
                    curr= getFolded(children, curr);
                }
                result.add(curr);
            } else if (fragment == null && curr.isDefaultPackage()) {
                result.add(curr);
            }
        }
    }

    /**
     * Returns the hierarchical packages inside a given folder.
     * @param folder The parent folder
     * @param result Collection where the resulting elements are added
     * @throws CoreException thrown when elements could not be accessed
     */
    private void getHierarchicalPackagesInFolder(IFolder folder, Collection<Object> result) throws CoreException {
        IResource[] resources= folder.members();
        for (int i= 0; i < resources.length; i++) {
            IResource resource= resources[i];
            if (resource instanceof IFolder) {
                IFolder curr= (IFolder) resource;
                IJavaElement element= JavaCore.create(curr);
                if (element instanceof IPackageFragment) {
                    if (fFoldPackages) {
                        IPackageFragment fragment= (IPackageFragment) element;
                        IPackageFragmentRoot root= (IPackageFragmentRoot) fragment.getParent();
                        element= getFolded(root.getChildren(), fragment);
                    }
                    result.add(element);
                }
            }
        }
    }

    public Object getHierarchicalPackageParent(IPackageFragment child) {
        String name= child.getElementName();
        IPackageFragmentRoot parent= (IPackageFragmentRoot) child.getParent();
        int index= name.lastIndexOf('.');
        if (index != -1) {
            String realParentName= name.substring(0, index);
            IPackageFragment element= parent.getPackageFragment(realParentName);
            if (element.exists()) {
                try {
                    if (fFoldPackages && isEmpty(element) && findSinglePackageChild(element, parent.getChildren()) != null) {
                        return getHierarchicalPackageParent(element);
                    }
                } catch (JavaModelException e) {
                    // ignore
                }
                return element;
            } else { // bug 65240
                IResource resource= element.getResource();
                if (resource != null) {
                    return resource;
                }
            }
        }
        if (parent.getResource() instanceof IProject) {
            return parent.getJavaProject();
        }
        return parent;
    }

    private static IPackageFragment getFolded(IJavaElement[] children, IPackageFragment pack) throws JavaModelException {
        while (isEmpty(pack)) {
            IPackageFragment collapsed= findSinglePackageChild(pack, children);
            if (collapsed == null) {
                return pack;
            }
            pack= collapsed;
        }
        return pack;
    }

    private static boolean isEmpty(IPackageFragment fragment) throws JavaModelException {
        return !fragment.containsJavaResources() && fragment.getNonJavaResources().length == 0;
    }

    private static IPackageFragment findSinglePackageChild(IPackageFragment fragment, IJavaElement[] children) {
        String prefix= fragment.getElementName() + '.';
        int prefixLen= prefix.length();
        IPackageFragment found= null;
        for (int i= 0; i < children.length; i++) {
            IJavaElement element= children[i];
            String name= element.getElementName();
            if (name.startsWith(prefix) && name.length() > prefixLen && name.indexOf('.', prefixLen) == -1) {
                if (found == null) {
                    found= (IPackageFragment) element;
                } else {
                    return null;
                }
            }
        }
        return found;
    }

    // ------ delta processing ------

    /**
     * Processes a delta recursively. When more than two children are affected the
     * tree is fully refreshed starting at this node.
     *
     * @param delta the delta to process
     * @param runnables the resulting view changes as runnables (type {@link Runnable})
     * @return true is returned if the conclusion is to refresh a parent of an element. In that case no siblings need
     * to be processed
     * @throws JavaModelException thrown when the access to an element failed
     */
    private boolean processDelta(IJavaElementDelta delta, Collection<Runnable> runnables) throws JavaModelException {

        int kind= delta.getKind();
        int flags= delta.getFlags();
        IJavaElement element= delta.getElement();
        int elementType= element.getElementType();


        if (elementType != IJavaElement.JAVA_MODEL && elementType != IJavaElement.JAVA_PROJECT) {
            IJavaProject proj= element.getJavaProject();
            if (proj == null || !proj.getProject().isOpen()) // TODO: Not needed if parent already did the 'open' check!
                return false;
        }

        if (elementType == IJavaElement.PACKAGE_FRAGMENT) {
            if ((flags & (IJavaElementDelta.F_CONTENT | IJavaElementDelta.F_CHILDREN)) == IJavaElementDelta.F_CONTENT) {
                // TODO: This should never be true for folders (F_CONTENT is only for files)
                if (!fIsFlatLayout) {
                    Object parent = getHierarchicalPackageParent((IPackageFragment) element);
                    if (!(parent instanceof IPackageFragmentRoot)) {
                        postRefresh(internalGetParent(parent), GRANT_PARENT, element, runnables);
                        return true;
                    }
                }
                // content change, without children info (for example resource added/removed to class folder package)
                postRefresh(internalGetParent(element), PARENT, element, runnables);
                return true;
            }

            if (!fIsFlatLayout) {
                if (kind == IJavaElementDelta.REMOVED) {
                    final Object parent = getHierarchicalPackageParent((IPackageFragment) element);
                    if (parent instanceof IPackageFragmentRoot) {
                        postRemove(element,  runnables);
                        return false;
                    } else {
                        postRefresh(internalGetParent(parent), GRANT_PARENT, element, runnables);
                        return true;
                    }
                } else if (kind == IJavaElementDelta.ADDED) {
                    final Object parent = getHierarchicalPackageParent((IPackageFragment) element);
                    if (parent instanceof IPackageFragmentRoot) {
                        if (fFoldPackages) {
                            postRefresh(parent, PARENT, element, runnables);
                            return true;
                        } else {
                            postAdd(parent, element, runnables);
                            return false;
                        }
                    } else {
                        postRefresh(internalGetParent(parent), GRANT_PARENT, element, runnables);
                        return true;
                    }
                }
                handleAffectedChildren(delta, element, runnables);
                return false;
            }
        }

        if (elementType == IJavaElement.COMPILATION_UNIT) {
            ICompilationUnit cu= (ICompilationUnit) element;
            if (!JavaModelUtil.isPrimary(cu)) {
                return false;
            }

            if (!getProvideMembers() && cu.isWorkingCopy() && kind == IJavaElementDelta.CHANGED) {
                return false;
            }

            if (kind == IJavaElementDelta.CHANGED && !isStructuralCUChange(flags)) {
                return false; // test moved ahead
            }

            if (!isOnClassPath(cu)) { // TODO: isOnClassPath expensive! Should be put after all cheap tests
                return false;
            }

        }

        if (elementType == IJavaElement.JAVA_PROJECT) {
            // handle open and closing of a project
            if ((flags & (IJavaElementDelta.F_CLOSED | IJavaElementDelta.F_OPENED)) != 0) {
                postRefresh(element, ORIGINAL, element, runnables);
                return false;
            }
            // if the class path has changed we refresh the entire project
            if ((flags & IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED) != 0) {
                postRefresh(element, ORIGINAL, element, runnables);
                return false;
            }
            // if added it could be that the corresponding IProject is already shown. Remove it first.
            // bug 184296
            if (kind == IJavaElementDelta.ADDED) {
                postRemove(element.getResource(), runnables);
                postAdd(element.getParent(), element, runnables);
                return false;
            }
        }

        if (kind == IJavaElementDelta.REMOVED) {
            Object parent= internalGetParent(element);
            if (element instanceof IPackageFragment) {
                // refresh package fragment root to allow filtering empty (parent) packages: bug 72923
                if (fViewer.testFindItem(parent) != null)
                    postRefresh(parent, PARENT, element, runnables);
                return true;
                
            } else if (element instanceof IPackageFragmentRoot) {
                // libs and class folders can show up twice (in library container and as resource at original location)
                IResource resource= element.getResource();
                if (resource != null && !resource.exists())
                    postRemove(resource, runnables);
            }

            postRemove(element, runnables);
            if (parent instanceof IPackageFragment)
                postUpdateIcon((IPackageFragment)parent, runnables);
            // we are filtering out empty subpackages, so we
            // a package becomes empty we remove it from the viewer.
            if (isPackageFragmentEmpty(element.getParent())) {
                if (fViewer.testFindItem(parent) != null)
                    postRefresh(internalGetParent(parent), GRANT_PARENT, element, runnables);
                return true;
            }
            return false;
        }

        if (kind == IJavaElementDelta.ADDED) {
            Object parent= internalGetParent(element);
            // we are filtering out empty subpackages, so we
            // have to handle additions to them specially.
            if (parent instanceof IPackageFragment) {
                Object grandparent= internalGetParent(parent);
                // 1GE8SI6: ITPJUI:WIN98 - Rename is not shown in Packages View
                // avoid posting a refresh to an invisible parent
                if (parent.equals(fInput)) {
                    postRefresh(parent, PARENT, element, runnables);
                } else {
                    // refresh from grandparent if parent isn't visible yet
                    if (fViewer.testFindItem(parent) == null)
                        postRefresh(grandparent, GRANT_PARENT, element, runnables);
                    else {
                        postRefresh(parent, PARENT, element, runnables);
                    }
                }
                return true;
            } else {
                if (element instanceof IPackageFragmentRoot
                        && ((IPackageFragmentRoot)element).getKind() != IPackageFragmentRoot.K_SOURCE) {
                    // libs and class folders can show up twice (in library container or under project, and as resource at original location)
                    IResource resource= element.getResource();
                    if (resource != null) {
                        Object resourceParent= super.internalGetParent(resource);
                        if (resourceParent != null) {
                            IJavaProject proj= element.getJavaProject();
                            if (fShowLibrariesNode || !resourceParent.equals(proj)) {
                                postAdd(resourceParent, resource, runnables);
                            }
                        }
                    }
                }
                postAdd(parent, element, runnables);
            }
        }

        if (elementType == IJavaElement.COMPILATION_UNIT || elementType == IJavaElement.CLASS_FILE) {
            if (kind == IJavaElementDelta.CHANGED) {
                // isStructuralCUChange already performed above
                postRefresh(element, ORIGINAL, element, runnables);
            }
            return false;
        }

        if (elementType == IJavaElement.PACKAGE_FRAGMENT_ROOT) {
            // the contents of an external JAR has changed
            if ((flags & IJavaElementDelta.F_ARCHIVE_CONTENT_CHANGED) != 0) {
                postRefresh(element, ORIGINAL, element, runnables);
                return false;
            }
            if ((flags & (IJavaElementDelta.F_CONTENT | IJavaElementDelta.F_CHILDREN)) == IJavaElementDelta.F_CONTENT) {
                // TODO: This should never be true for folders (F_CONTENT is only for files)
                // content change, without children info (for example resource added/removed to class folder package)
                postRefresh(internalGetParent(element), PARENT, element, runnables);
                return true;
            }

            // the source attachment of a JAR has changed
            if ((flags & (IJavaElementDelta.F_SOURCEATTACHED | IJavaElementDelta.F_SOURCEDETACHED)) != 0)
                postUpdateIcon(element, runnables);

            if (isClassPathChange(delta)) {
                 // throw the towel and do a full refresh of the affected java project.
                postRefresh(element.getJavaProject(), PROJECT, element, runnables);
                return true;
            }
        }

        handleAffectedChildren(delta, element, runnables);
        return false;
    }

    private static boolean isStructuralCUChange(int flags) {
        // No refresh on working copy creation (F_PRIMARY_WORKING_COPY)
        return (flags & IJavaElementDelta.F_CHILDREN) != 0 || (flags & (IJavaElementDelta.F_CONTENT | IJavaElementDelta.F_FINE_GRAINED)) == IJavaElementDelta.F_CONTENT;
    }

    /* package */ void handleAffectedChildren(IJavaElementDelta delta, IJavaElement element, Collection<Runnable> runnables) throws JavaModelException {
        int count= 0;

        IResourceDelta[] resourceDeltas= delta.getResourceDeltas();
        if (resourceDeltas != null) {
            for (int i= 0; i < resourceDeltas.length; i++) {
                int kind= resourceDeltas[i].getKind();
                if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED) {
                    count++;
                }
            }
        }
        IJavaElementDelta[] affectedChildren= delta.getAffectedChildren();
        for (int i= 0; i < affectedChildren.length; i++) {
            int kind= affectedChildren[i].getKind();
            if (kind == IJavaElementDelta.ADDED || kind == IJavaElementDelta.REMOVED) {
                count++;
            }
        }

        if (count > 1) {
            // more than one child changed, refresh from here downwards
            if (element instanceof IPackageFragment) {
                // a package fragment might become non empty refresh from the parent
                IJavaElement parent= (IJavaElement) internalGetParent(element);
                // 1GE8SI6: ITPJUI:WIN98 - Rename is not shown in Packages View
                // avoid posting a refresh to an invisible parent
                if (element.equals(fInput)) {
                    postRefresh(element, ORIGINAL, element, runnables);
                } else {
                    postRefresh(parent, PARENT, element, runnables);
                }
            } else if (element instanceof IPackageFragmentRoot) {
                Object toRefresh= internalGetParent(element);
                postRefresh(toRefresh, ORIGINAL, toRefresh, runnables);
            } else {
                postRefresh(element, ORIGINAL, element, runnables);
            }
            return;
        }
        if (resourceDeltas != null) {
            for (int i= 0; i < resourceDeltas.length; i++) {
                if (processResourceDelta(resourceDeltas[i], element, runnables)) {
                    return; // early return, element got refreshed
                }
            }
        }
        for (int i= 0; i < affectedChildren.length; i++) {
            if (processDelta(affectedChildren[i], runnables)) {
                return; // early return, element got refreshed
            }
        }
    }

    protected void processAffectedChildren(IJavaElementDelta[] affectedChildren, Collection<Runnable> runnables) throws JavaModelException {
        for (int i= 0; i < affectedChildren.length; i++) {
            processDelta(affectedChildren[i], runnables);
        }
    }

    private boolean isOnClassPath(ICompilationUnit element) {
        IJavaProject project= element.getJavaProject();
        if (project == null || !project.exists())
            return false;
        return project.isOnClasspath(element);
    }

    /**
     * Updates the package icon
     * @param element the element to update
     * @param runnables the resulting view changes as runnables (type {@link Runnable})
     */
     private void postUpdateIcon(final IJavaElement element, Collection<Runnable> runnables) {
         runnables.add(new Runnable() {
            public void run() {
                // 1GF87WR: ITPUI:ALL - SWTEx + NPE closing a workbench window.
                fViewer.update(element, new String[]{IBasicPropertyConstants.P_IMAGE});
            }
        });
     }

    /**
     * Process a resource delta.
     *
     * @param delta the delta to process
     * @param parent the parent
     * @param runnables the resulting view changes as runnables (type {@link Runnable})
     * @return true if the parent got refreshed
     */
    private boolean processResourceDelta(IResourceDelta delta, Object parent, Collection<Runnable> runnables) {
        int status= delta.getKind();
        int flags= delta.getFlags();

        IResource resource= delta.getResource();
        // filter out changes affecting the output folder
        if (resource == null)
            return false;

        // this could be optimized by handling all the added children in the parent
        if ((status & IResourceDelta.REMOVED) != 0) {
            if (parent instanceof IPackageFragment) {
                // refresh one level above to deal with empty package filtering properly
                postRefresh(internalGetParent(parent), PARENT, parent, runnables);
                return true;
            } else {
                postRemove(resource, runnables);
                return false;
            }
        }
        if ((status & IResourceDelta.ADDED) != 0) {
            if (parent instanceof IPackageFragment) {
                // refresh one level above to deal with empty package filtering properly
                postRefresh(internalGetParent(parent), PARENT, parent, runnables);
                return true;
            } else {
                postAdd(parent, resource, runnables);
                return false;
            }
        }
        if ((status & IResourceDelta.CHANGED) != 0) {
            if ((flags & IResourceDelta.TYPE) != 0) {
                postRefresh(parent, PARENT, resource, runnables);
                return true;
            }
        }
        // open/close state change of a project
        if ((flags & IResourceDelta.OPEN) != 0) {
            postProjectStateChanged(internalGetParent(parent), runnables);
            return true;
        }
        IResourceDelta[] resourceDeltas= delta.getAffectedChildren();

        int count= 0;
        for (int i= 0; i < resourceDeltas.length; i++) {
            int kind= resourceDeltas[i].getKind();
            if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED) {
                count++;
                if (count > 1) {
                    postRefresh(parent, PARENT, resource, runnables);
                    return true;
                }
            }
        }
        for (int i= 0; i < resourceDeltas.length; i++) {
            if (processResourceDelta(resourceDeltas[i], resource, runnables)) {
                return false; // early return, element got refreshed
            }
        }
        return false;
    }

    public void setIsFlatLayout(boolean state) {
        fIsFlatLayout= state;
    }

    public void setShowLibrariesNode(boolean state) {
        fShowLibrariesNode= state;
    }

    private void postRefresh(Object root, int relation, Object affectedElement, Collection<Runnable> runnables) {
        // JFace doesn't refresh when object isn't part of the viewer
        // Therefore move the refresh start down to the viewer's input
        if (isParent(root, fInput) || root instanceof IJavaModel)
            root= fInput;
        List<Object> toRefresh= new ArrayList<Object>(1);
        toRefresh.add(root);
        augmentElementToRefresh(toRefresh, relation, affectedElement);
        postRefresh(toRefresh, true, runnables);
    }

    /**
     * Can be implemented by subclasses to add additional elements to refresh
     *
     * @param toRefresh the elements to refresh
     * @param relation the relation to the affected element ({@link #GRANT_PARENT}, {@link #PARENT}, {@link #ORIGINAL}, {@link #PROJECT})
     * @param affectedElement the affected element
     */
    protected void augmentElementToRefresh(List<Object> toRefresh, int relation, Object affectedElement) {
    }

    private boolean isParent(Object root, Object child) {
        Object parent= getParent(child);
        if (parent == null)
            return false;
        if (parent.equals(root))
            return true;
        return isParent(root, parent);
    }

    protected void postRefresh(final List<Object> toRefresh, final boolean updateLabels, Collection<Runnable> runnables) {
        runnables.add(new Runnable() {
            public void run() {
                Object[] elements= toRefresh.toArray();
                for (int i= 0; i < elements.length; i++) {
                    Object element= elements[i];
                    if (element == null || fViewer.testFindItems(element).length > 0) {
                        fViewer.refresh(element, updateLabels);
                    }
                }
            }
        });
    }

    protected void postAdd(final Object parent, final Object element, Collection<Runnable> runnables) {
        runnables.add(new Runnable() {
            public void run() {
                Widget[] items= fViewer.testFindItems(element);
                for (int i= 0; i < items.length; i++) {
                    Widget item= items[i];
                    if (item instanceof TreeItem && !item.isDisposed()) {
                        TreeItem parentItem= ((TreeItem) item).getParentItem();
                        if (parentItem != null && !parentItem.isDisposed() && parent.equals(parentItem.getData())) {
                            return; // no add, element already added (most likely by a refresh)
                        }
                    }
                }
                fViewer.add(parent, element);
            }
        });
    }

    protected void postRemove(final Object element, Collection<Runnable> runnables) {
        runnables.add(new Runnable() {
            public void run() {
                if (fViewer.testFindItems(element).length > 0) {
                    fViewer.remove(element);
                }
            }
        });
    }

    protected void postProjectStateChanged(final Object root, Collection<Runnable> runnables) {
        runnables.add(new Runnable() {
            public void run() {
                fViewer.refresh(root, true);
                // trigger a synthetic selection change so that action refresh their
                // enable state.
                fViewer.setSelection(fViewer.getSelection());
            }
        });
    }


    /*
     * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
     */
    public void propertyChange(PropertyChangeEvent event) {
        if (arePackagesFoldedInHierarchicalLayout() != fFoldPackages){
            fFoldPackages= arePackagesFoldedInHierarchicalLayout();
            if (fViewer != null && !fViewer.getControl().isDisposed()) {
                fViewer.getControl().setRedraw(false);
                Object[] expandedObjects= fViewer.getExpandedElements();
                fViewer.refresh();
                fViewer.setExpandedElements(expandedObjects);
                fViewer.getControl().setRedraw(true);
            }
        }
    }
}
